iT邦幫忙

2024 iThome 鐵人賽

DAY 17
0
JavaScript

Signal API in Angular系列 第 17

Day 17 - viewChild 函數簡介

  • 分享至 

  • xImage
  •  

從 Angular 17 開始,團隊開始將 decorators 遷移到 signals。第一個是 @Input decorator 和對應的 signal input 函數。 此後以下查詢裝飾器已移轉。

  • @ViewChild -> viewchild()
  • @ViewChildren -> viewChildren()
  • @contentChild -> contentChild()
  • @contentChildren -> contentChildren()

今天,我將介紹 viewChild,因為我經常將它與表單、HTML 元素、ngTemplates 和組件一起使用。

ViewChild decorator 和 viewChild 函數的區別

  • ViewChild decorator 傳回一個實例,而 viewChild 函數傳回一個 signal
  • ViewChild decorator 的 static 屬性從 viewChild 函數中刪除

必需和可選的 viewChild

  • viewChild.required() 表示該元素在組件中至少出現一次,並且檢索第一個元素。 此函數的傳回類型是 Signal。
  • viewChild() 表示該元素未出現在範本中且可能是未定義的。函數的傳回類型為 Signal<T | undefined>。

在以下用例中,我將展示 viewChild 如何透過範本變數 (template variable) 查詢 NgForm、ngTemplates, directives 和 Angular 組件。

例子 1:透過範本變數查詢 NgForm

@Component({
 selector: 'app-address-form',
 standalone: true,
 imports: [FormsModule, JsonPipe],
 template: `
   <h3>ViewChild to obtain a NgForm and ElementRef</h3>
   <form #f="ngForm">
     <div>
       <label for="firstName">
         <span>First Name: </span>
         <input id="firstName" name="firstName" type="text" required [(ngModel)]="formValues().firstName" size="50" #fn="ngModel">
       </label>

       @if(fn.dirty && fn.errors?.['required']) {
         <p class="error">First name is required.</p>
       }
     </div>
     <div>
       <label for="lastName">
         <span>Last Name:: </span>
         <input id="lastName" name="lastName" type="text" required [(ngModel)]="formValues().lastName" size="50" #ln="ngModel">
       </label>

       @if(ln.dirty && ln.errors?.['required']) {
         <p class="error">Last name is required.</p>
       }
     </div>     
   </form>
 `,
})
export class AddressFormComponent implements OnInit {}

AddressFormComponent 是一個簡單的 template-driven 表單,帶有名字和姓氏輸入欄位。

export type FormValues = {
 firstName: string;
 lastName: string;
}

export const initialValues = {
 firstName: 'test',
 lastName: 'me',
}

export class AddressFormComponent implements OnInit {
 form = viewChild.required('f', { read: NgForm });

 formValues = signal<FormValues>(initialValues);
 isFormValid = signal(false);
 isFormSubmitted = signal(false);
}

viewChild.required 函數從範本中查詢 NgForm。 Selector是 f,因為 NgForm 的範本變數 (template variable) 是 #f。 第二個參數 { read: NgForm } 確保檢索 NgFormformValues 是儲存表單值的 signalisFormValid signal 儲存表單是否有效。 isFormSubscribed signal 儲存表單是否已提交。

constructor() {
   effect((OnCleanUp) => {
     const formValueChanges$ = this.form().form.valueChanges.pipe(
       debounceTime(0)
     );

     const sub = formValueChanges$.subscribe((values) => {
       this.formValues.set(values);
       this.isFormValid.set(this.form().valid || false);
       this.isFormSubmitted.set(false);
     });

     const sub2 = this.form().ngSubmit.subscribe(() => {
       this.isFormSubmitted.set(true);
     });

     OnCleanUp(() => {
       sub.unsubscribe();
       sub2.unsubscribe();
     });
   });
 }

當表單值更新時, effect 執行邏輯來設定 signal 的值。

viewModel = computed(() => ({
   values: this.formValues(),
   isFormValid: this.isFormValid(),
   isFormSubmitted: this.isFormSubmitted(),
 }));

 get vm() {
   return this.viewModel();
 }

建立 view model 以存取 HTML 範本中的 signal 值。

<form #f="ngForm">    
   <button type="submit" [disabled]="!vm.isFormValid">Submit</button>
</form>

@if (vm.isFormSubmitted) {
   <pre>
       values: {{ vm.values | json }}
   </pre>
}

vm.isFormValidtrue 時,表單有效,並且啟用 Submit 按鈕。當 vm.isFormSubmittedtrue 時,表單提交發生並顯示表單物件值。

例子 2:使用範本變數 (template variable) 查詢 ngTemplates

@Component({
 selector: 'app-permission',
 standalone: true,
 imports: [FormsModule, NgTemplateOutlet],
 template: `
   <ng-template #admin>
     <p>You are an admin. You can do the following task(s):</p>
     <ul>
       <li>Add an account</li>
       <li>Delete an account</li>
       <li>Update an account</li>
       <li>Upgrade role</li>
       <li>Downgrade role</li>
     </ul>
   </ng-template>

   <ng-template #user>
     <p>You are a user. You can do the following task(s):</p>
     <ul>
       <li>View accounts</li>
     </ul>
   </ng-template>

   <ng-container *ngTemplateOutlet="template()" />
 `,
})
export default class PermissionComponent {
 hasPermission = signal(false);
}

此例子將單選按鈕綁定到 hasPermission signal。 當 hasPermissiontrue 時,組件顯示 admin ngTemplate。當 hasPermissionfalse 時,組件顯示 user ngTemplate。

userTemplate = viewChild.required('user', { read: TemplateRef });

viewChild.required 查詢具有範本變數 #user 的元素,並且 { read: TemplateRef } 檢索具有 TemplateRef 類型的元素。

adminTemplate = viewChild.required('admin', { read: TemplateRef });

viewChild.required 使用範本變數 (template variable) #admin 查詢 ngTemplate

template = computed(() => this.hasPermission() ? this.adminTemplate() : this.userTemplate());

範本根據 hasPermission signal value 決定 template computed signal. template 決定要顯示的 ngTemplate。

<ng-container *ngTemplateOutlet="template()" />

將範本指派給 ngTemplateOutlet directive,由 ngContainer 顯示它。

例子 3:按類型查詢 Angular 組件

import { Component, ChangeDetectionStrategy, signal, computed } from '@angular/core';

const imgURL = 'https://picsum.photos';

@Component({
 selector: 'app-photo',
 standalone: true,
 template: `
   <div class="photo">
     <img [src]="img()" alt="Random picture" />
   </div>
 `,
})
export default class PhotoComponent {
 width = signal(300);
 height = signal(200);
 random = signal(Date.now());

 img = computed(() => `${imgURL}/${this.width()}/${this.height()}?random=${this.random()}`)
}

PhotoComponent 組件有三個 signals 來定義圖片 URL 的寬度、高度和隨機種子。 當任何 signal value 更新時,img computed signal 會產生新的圖像 URL。

import { ChangeDetectionStrategy, Component, viewChild } from '@angular/core';
import PhotoComponent from './photo.component';

@Component({
 selector: 'app-root',
 standalone: true,
 imports: [PhotoComponent],
 template: `
   <h3>ViewChild to obtain PhotoComponent</h3>
   <app-photo />
   <button (click)="changeImage()">Change image</button>
   <button (click)="increaseDimensions()">Make it 400 x 400</button>
 `,
})
export class App {
 photo = viewChild.required(PhotoComponent);

 changeImage() {
   const photo = this.photo();
   photo.random.set(Date.now());
 }

 increaseDimensions() {
   const photo = this.photo();
   photo.width.set(400);
   photo.height.set(400);
   photo.random.set(Date.now());
 }
}

App 組件使用 viewChild 函數按類型查詢 PhotoComponentApp 組件有兩個按鈕,點擊時可設定 signal。 然後,`PhotoComponent 在 HTML 範本中載入新圖像

例子 4:按 Dirctive 類型查詢 Directive

import { Directive, ElementRef, inject, model, OnInit } from "@angular/core";

@Directive({
 selector: '[appLabel]',
 standalone: true,
 host: {
   '(mouseenter)' : "updateColor(appLabel())",
   '(mouseleave)' : "updateColor('black')"
 }
})
export class AppBoldLabelDirective {
 nativeElement = inject<ElementRef<HTMLLabelElement>>(ElementRef).nativeElement;

 appLabel = model('red');

 updateColor(value: string) {
   this.nativeElement.style.color = value;
 }
}

定義 AppBoldLabelDirective 以變更 mouseentermouseleave 事件上的標籤顏色。在 PermissionComponent 中,指定標籤元素的 directive.

imports: [FormsModule, NgTemplateOutlet, AppBoldLabelDirective],
 template: `
   <label for="hasPermission" [appLabel]="'red'">Yes</label><br>
   <label for="noPermission" [appLabel]="'darkgoldenrod'">No</label><br>
`,

export default class PermissionComponent implements AfterViewInit {

directive = viewChild(AppBoldLabelDirective);

ngAfterViewInit() {
   const directive = this.directive();
   if (directive) {
     directive.nativeElement.style.color = 'goldenrod';
   }
}
}

出於示範原因,該組件會查詢第一個 AppBoldLabelDirective directive,並且 ngAfterViewInit lifecycle hook 將文字顏色變更為金黃色。 當滑鼠懸停在標籤上時,標籤文字的顏色會改變。當滑鼠離開標籤時,標籤文字會變成黑色。

結論:

  • viewChild 可以查詢 elements、ngTemplates、directives 和 components。第一個參數是一個選擇器,它是 template variable 或 type。
  • read 屬性指定 viewChild 要傳回的元素類型
  • 如果範本中至少存在一個元素,我們可以使用viewChild.required函數。否則,我們應該使用viewChild 函數,它可以傳回 signal 或 undefined。
  • viewChild 函數的 selector 符合範本中的多個元素時,傳回第一個。
  • 如果 viewChild.required 無法查詢某個元素,則函數將拋出錯誤。

鐵人賽的第 17 天就這樣結束了。

結論:


上一篇
Day 16 - 組件與model input之間的溝通
下一篇
Day 18 - viewChild 函數的高階使用者案例 1 - 以程式設計方式建立 Angular 組件
系列文
Signal API in Angular39
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言